In [3]:
from sklearn.naive_bayes import MultinomialNB, ComplementNB
from sklearn.linear_model import LogisticRegression, SGDClassifier, RidgeClassifier
from sklearn.ensemble import RandomForestClassifier
from sklearn.neighbors import KNeighborsClassifier
from sklearn.svm import LinearSVC

from sklearn.preprocessing import LabelEncoder
from sklearn.feature_extraction.text import TfidfVectorizer
from sklearn.pipeline import Pipeline
from sklearn.metrics import accuracy_score, classification_report, confusion_matrix
from sklearn.model_selection import (
    train_test_split, cross_val_score, StratifiedKFold,
)

import numpy as np
import pandas as pd
np.random.seed(42)

from yellowbrick.model_selection import LearningCurve
import plotly.express as px

from tqdm import tqdm
import string
import time
import re

import nltk
from nltk.corpus import stopwords

from natasha import Segmenter, MorphVocab, NewsEmbedding, NewsMorphTagger, Doc
import optuna

import torch
from transformers import (
    AutoModelForSequenceClassification, AutoTokenizer, TrainingArguments, Trainer
)
import opendatasets as od
from datasets import Dataset
import evaluate
In [4]:
import ssl
ssl._create_default_https_context = ssl._create_unverified_context


Работу выполнил: Таратин Артём ПМ22-1
Датасет: Russian Social Media Text Classification


Введение

На данный момент машинное обучение играет ключевую роль в обработке больших данных в задачах обработки естественного языка, в том числе и распознавании тем текстов. Способность алгоритмов эффективно выполнять человеческую работу в этой сфере открывает новые горизонты для большого количества сфер деятельности.

Проблемой исследования является достижение хороших показателей метрик в данной задаче, что в силу многообразия языковых конструкций является довольно непростой задачей.

В работе раскрывается тема классификации текстов с предобработкой датасета и применением различных способов, моделей машинного обучения.

Целью работы будет разработка модели для классификации текстов по 13 категориям с максимально возможным значением метрики accuracy_score() на датасете Russian Social Media Text Classification с использованием различного инструментария.

Используются методы предобработки и векторизации текста, различные модели машинного обучения из библиотеки scikit-learn, модели, основанные на нейронных сетях, а также и другие инструменты машинного обучения.

Работа поможет понять теоретическую часть механизмов классификации текстов, а также продемонстрирует их применение. Разработанные методы смогут найти применение в любых других задачах, связанных с классификацией русских текстов из социальных сетей.


Глава 1. Основа

1.1 Введение в NLP и классификацию текстов¶

NLP (Natural Language Processing) - Обработка естественного языка это направление в машинном обучении, фокусирующееся на обработке, распознавании и генерации текста для различного рода задач. NLP тесно связана не только с машинным обучением, но и с лингвистикой.

На данный момент, NLP используется повсюду: Chat GPT, YandexGPT, поиск в Яндексе, Google, персональный помощник Алиса, Маруся и т.д. Такой подход позволяет значительно облегчить и упростить некоторые задачи, что также используется в коммерции.

Наиболее известными задачами NLP являются:

  • Text Classification – классификация текстов по заранее определённым категориям
  • Question Answering – ответы на вопросы по заданному контексту
  • Translation – перевод предложений с различных языков
  • Summarization – сжатие длинных документов в краткое изложение
  • Text Generation – предоставляют возможность написания текста по любому запросу

1.2 Методы векторизации текста¶

Векторизация подразумевает под собой преобразование входных данных в векторы или тензоры действительных чисел, которые понятны моделям машинного обучения. Существует большое количество методов векторизации текста, все они отличаются между собой механизмом действия.

Мешок слов¶

Самый простой из всех существующих способов векторизации.

Алгоритм состоит из несколько шагов:

  • Токенизация – из предложения извлекаются слова (токены)
  • Создание словаря – создаётся одномерный массив из всех уникальных токенов
  • Создание вектора – каждому токену из предложения присваивается 1, если токен присутствует в предложении, 0 иначе

TF-IDF¶

TF-IDF (Term Frequency-Inverse Document Frequency) – это числовой статистический показатель, который учитывает важность слова для документа. Алгоритм основан также на частотности как и мешок слов, но в нём используются более сложные методы вычисления.

Разберем TF-IDF по словам. TF означает Term Frequency (частотность термина), то есть нормализированный показатель частоты.

$ TF = \frac{Частота\ использования\ слова\ в\ документе}{Общее\ количество\ слов\ в\ документе} $

IDF означает Inversed Document Frequency (обратная частота документа).

$ DF = \frac{Документы\ содержащие\ слово}{Общее\ количество\ документов} $

$ IDF = \log(\frac{Документы\ содержащие\ слово}{Общее\ количество\ документов}) $

В итоге TF-IDF можно записать так:

$ TF-IDF = TF * IDF $

1.3 Описание используемых библиотек¶

  1. sklearn, torch, transformers: Библиотеки машинного обучения

  2. opendatasets, datasets: Парсинг и работа с датасетами

  3. evaluate: Метрики для оценки моделей

  4. numpy и pandas: Анализ и обработка данных

  5. yellowbrick: Визуализация результатов машинного обучения

  6. plotly: Визуализация данных

  7. nltk: Обработка естественного языка

  8. natasha: Обработка текста на русском языке

  9. optuna: Оптимизация параметров моделей

  10. tqdm: Отображение индикатора выполнения


Глава 2. Работа с датасетом

2.1 Описание Датасета¶

Оригинал:¶

VKontakte communities can belong to one of several predefined categories. But even among the sports communities there is a fairly strong division by subject! The same authors can write about only one sport or at once about a large number. Based on a given set of posts, determine the topic – what kind of sport is being discussed in the selected community?

Перевод:¶

Сообщества ВКонтакте могут относиться к одной из нескольких предопределенных категорий. Но даже среди спортивных сообществ существует довольно строгое разделение по тематике! Одни и те же авторы могут писать только об одном виде спорта или сразу о большом количестве. Основываясь на заданном наборе постов, определите тему – какой вид спорта обсуждается в выбранном сообществе?

Список доступных категорий (13):¶

  • athletics – легкая атлетика,
  • autosport – автоспорт,
  • basketball – баскетбол,
  • boardgames – настольные игры,
  • esport – киберспорт,
  • extreme – экстрим,
  • football – футбол,
  • hockey – хоккей,
  • martial_arts – боевые искусства,
  • motosport – автоспорт,
  • tennis – теннис,
  • volleyball – волейбол,
  • winter_sport – зимний спорт

Функция потерь выглядит так:¶

def score(true, pred, n_samples):
    counter = 0
    if true == pred:
        counter += 1
    else:
        counter -= 1
    return counter / n_samples

2.2 Анализ, Предобработка данных и вывод основных характеристик¶

Загружаем датасет с kaggle.com

In [5]:
od.download('https://www.kaggle.com/datasets/mikhailma/russian-social-media-text-classification')
Skipping, found downloaded files in "./russian-social-media-text-classification" (use force=True to force download)

Считываем данные и выводим первые 5 строк

In [6]:
train_data = pd.read_csv('./russian-social-media-text-classification/train.csv')
test_data = pd.read_csv('./russian-social-media-text-classification/test.csv')
train_data.head()
Out[6]:
oid category text
0 365271984 winter_sport Волшебные фото Виктория Поплавская ЕвгенияМедв...
1 503385563 extreme Возвращение в подземелье Треша 33 Эйфория тупо...
2 146016084 football Лучшие чешские вратари – Доминик Доминатор Гаш...
3 933865449 boardgames Rtokenoid Warhammer40k валрак решил нас подкор...
4 713550145 hockey Шестеркин затаскивает Рейнджерс в финал Восточ...
In [7]:
train_data.shape, train_data.category.unique().size
Out[7]:
((38740, 3), 13)
In [8]:
train_data.category.unique()
Out[8]:
array(['winter_sport', 'extreme', 'football', 'boardgames', 'hockey',
       'esport', 'athletics', 'motosport', 'basketball', 'tennis',
       'autosport', 'martial_arts', 'volleyball'], dtype=object)
In [9]:
train_data.dtypes
Out[9]:
oid          int64
category    object
text        object
dtype: object
In [10]:
train_data.isna().sum()
Out[10]:
oid         0
category    0
text        0
dtype: int64

Лишних столбцов нету, также как и выбросов

In [11]:
train_data.duplicated('text').sum()
Out[11]:
2966

Но в то же время присутствуют дубликаты, которые лучше убрать, либо оставить первый

In [12]:
train_data.drop_duplicates('text', keep='first', inplace=True)  # Оставляем первый экземпляр
train_data.duplicated('text').sum()
Out[12]:
0

Используем LabelEncoder() для преобразования целевого столбца

In [13]:
le = LabelEncoder()
train_data['category_le'] = le.fit_transform(train_data.category)
train_data.head()
Out[13]:
oid category text category_le
0 365271984 winter_sport Волшебные фото Виктория Поплавская ЕвгенияМедв... 12
1 503385563 extreme Возвращение в подземелье Треша 33 Эйфория тупо... 5
2 146016084 football Лучшие чешские вратари – Доминик Доминатор Гаш... 6
3 933865449 boardgames Rtokenoid Warhammer40k валрак решил нас подкор... 3
4 713550145 hockey Шестеркин затаскивает Рейнджерс в финал Восточ... 7

Теперь выведем распределение классов и укажем количество их использований

In [14]:
sr = train_data.category.value_counts(sort=True, ascending=True)
xp, yp = np.array(sr), np.array(sr.index)

fig = px.bar(
    x=xp, y=yp, orientation='h', text=yp, text_auto=True, color=xp,
    title='Количество Текстов по Категориям', height=700,
    color_continuous_scale=px.colors.sequential.dense,
)
fig.update_layout(
    xaxis_tickvals=np.arange(0, 3001, 250), title_x=0.5,
    xaxis_title='Количество', yaxis_title='Категория', showlegend=False,
)
fig.show()

Построим гистограмму распределения количества символов в предложениях

In [15]:
train_data['length'] = train_data['text'].str.len()
train_data.head(3)
Out[15]:
oid category text category_le length
0 365271984 winter_sport Волшебные фото Виктория Поплавская ЕвгенияМедв... 12 65
1 503385563 extreme Возвращение в подземелье Треша 33 Эйфория тупо... 5 246
2 146016084 football Лучшие чешские вратари – Доминик Доминатор Гаш... 6 704
In [16]:
fig = px.histogram(
    x=train_data['length'], title='Распределение Количества Символов', height=500,
    color_discrete_sequence=[px.colors.sequential.Blues[-3]]
)
fig.update_layout(
    xaxis_tickvals=np.arange(0, 3001, 250), xaxis_title='Количество',
    yaxis_title='Категория', title_x=0.5,
)
fig.show()

Согласно графику, распределение категорий довольно близко к равномерному. В таком случае можно использовать метрику accuracy_score() т.к. даже при выборе самого популярного класса точность будет достаточно низкой. В любом случае, метрика score(), предложенная создателями датасета, по своему механизму работы очень похожа на accuracy_score(). Теперь посмотрим на самые популярные и менее популярные слова в текстах.

In [17]:
word_list = re.findall(r'\b\w{2,}\b', ' '.join(train_data.text.to_numpy()).lower())
df_words = pd.DataFrame(word_list, columns=['word']).groupby('word').size()
df_words = df_words.reset_index(name='count').sort_values('count', ascending=False)
pd.concat([df_words.head(25).reset_index(drop=True), df_words.tail(25).reset_index(drop=True)], axis=1)
Out[17]:
word count word count
0 на 39638 манипулировал 1
1 не 23635 мансабын 1
2 что 19917 мансап 1
3 33 19756 манфорд 1
4 по 14054 манси 1
5 за 11337 мануэля 1
6 это 10827 мануэлю 1
7 для 9919 мануэла 1
8 из 9508 мануальный 1
9 но 9212 мануалы 1
10 как 8540 мануала 1
11 мы 7755 мантуана 1
12 все 7677 мантуан 1
13 он 7371 мантикору 1
14 от 6381 мантикора 1
15 то 6172 мантика 1
16 до 5168 мансуровичем 1
17 будет 4750 мансурович 1
18 его 4666 мансуров 1
19 так 4493 мансури 1
20 после 4167 мансур 1
21 вы 4156 мансолдо 1
22 2022 4150 мансийским 1
23 tokentokenoid 4144 мансийский 1
24 если 4101 龍王 1

В дополнение ко всему, нужно исключить слова, которые не несут никакой смысловой нагрузки. Для этого используем библиотеку nltk и nltk.corpus.stopwords.

In [18]:
nltk.download('stopwords')
russian_stopwords = stopwords.words('russian')
russian_stopwords[:10], len(russian_stopwords)
[nltk_data] Downloading package stopwords to /Users/admin/nltk_data...
[nltk_data]   Package stopwords is already up-to-date!
Out[18]:
(['и', 'в', 'во', 'не', 'что', 'он', 'на', 'я', 'с', 'со'], 151)

Со стоп словами

In [19]:
fig = px.treemap(df_words.head(275), path=['word'], values='count')
fig.show()

После удаления стоп слов

In [20]:
fig = px.treemap(df_words[~df_words.word.isin(russian_stopwords)].head(275), path=['word'], values='count')
fig.show()

Одними из самых популярных слов оказались предлоги, они не влияют на качество классификации, так как присутствуют в большинстве классов. Но даже после удаления стоп слов можно заметить числа и слова с основой token, всё это также не несёт смысловой нагрузки. Конечный вариант соберём на этапе предобработки датасета с кастомными фильтрами.

In [21]:
fig = px.histogram(
    x=np.log2(df_words['count']), title='Распределение Слов по Частоте Употребления', height=500,
    color_discrete_sequence=[px.colors.sequential.Blues[-3]], nbins=30
)
fig.update_layout(
    xaxis_tickvals=np.arange(0, 16, 1), xaxis_title='Логарифм количества употреблений слов (X)',
    yaxis_title='Количество слов употреблённых 2^X раз', title_x=0.5,
)
fig.show()

Большинство слов используются всего 1-4 раз.

Теперь посмотрим на распределение количества символов для каждого класса.

In [22]:
fig = px.box(data_frame=train_data, x='category', y='length', color='category')
fig.update_traces(marker_color=px.colors.sequential.Blues[-3])
fig.update_layout(
    title='Количество Символов в Предложениях', showlegend=False,
    xaxis_title='Класс', yaxis_title='Количество Символов', title_x=0.5,
)
fig.show()

2.3 Финальная Подготовка Датасета¶

Ещё раз инициализируем стоп слова

In [23]:
russian_stopwords = stopwords.words('russian')
russian_stopwords.extend(['это', 'tokentokenoid', 'rtokenoid', 'tokenoid', 'tokenoidtokenoid', 'votokenoid', 'evgentokenoid', '–'])
russian_stopwords[:10], len(russian_stopwords)
Out[23]:
(['и', 'в', 'во', 'не', 'что', 'он', 'на', 'я', 'с', 'со'], 159)
In [24]:
unallowed_chars = list(chr(i) for i in [*np.arange(ord('а'), ord('я')+1), *np.arange(ord('А'), ord('Я')+1), ord('ё'), ord('Ё')]) + list(string.ascii_letters)
russian_stopwords.extend(string.punctuation)
russian_stopwords.extend(unallowed_chars)
russian_stopwords[:10], len(russian_stopwords)
Out[24]:
(['и', 'в', 'во', 'не', 'что', 'он', 'на', 'я', 'с', 'со'], 309)

Пишем собственную функцию для токенизации предложений, удаления стоп слов, знаков и лемматизации слов

In [25]:
segmenter = Segmenter()
morph_vocab = MorphVocab()
emb = NewsEmbedding()
morph_tagger = NewsMorphTagger(emb)
In [26]:
def is_sublist(lst1, lst2):
    return set(lst1) <= set(lst2)

def tokenize(text):
    doc = Doc(text)
    doc.segment(segmenter)
    doc.tag_morph(morph_tagger)

    for token in doc.tokens:
        token.lemmatize(morph_vocab)
    
    tokens = [
        i.lemma.strip() for i in doc.tokens
        if i.lemma.strip() not in russian_stopwords and
        not i.lemma.strip().isnumeric()
    ]

    return tokens

Добавляем новый столбец с обработанным текстом

In [29]:
%%time
train_data['text_tokenized'] = list(map(tokenize, train_data.text))
train_data['text_tokenized_joined'] = train_data['text_tokenized'].str.join(' ')
test_data['text_tokenized'] = list(map(tokenize, test_data.text))
test_data['text_tokenized_joined'] = test_data['text_tokenized'].str.join(' ')
CPU times: user 44min 23s, sys: 31.1 s, total: 44min 54s
Wall time: 6min 14s

Поделим данные на тренировочную, валидационную и тестовую выборки.

In [30]:
X, y = train_data.text_tokenized.to_numpy(), train_data.category_le.to_numpy()
X_train, X_test_val, y_train, y_test_val = train_test_split(X, y, test_size=0.4, random_state=42, stratify=y)
X_test, X_val, y_test, y_val = train_test_split(X_test_val, y_test_val, test_size=0.5, random_state=42, stratify=y_test_val)
del X_test_val, y_test_val  # удалим эти переменные, чтобы их после случайно не использовать


Глава 3. Обучение моделей для классификации текстов

Для векторизации текстов и оценки важности слова в контексте документа будем использовать статистическую меру TF-IDF. Для оптимизации работы используем функцию для обучения и оценки моделей с использованием Pipeline.

In [31]:
models = [
    LogisticRegression(n_jobs=-1, random_state=42),
    ComplementNB(),
    SGDClassifier(n_jobs=-1, random_state=42),
    RandomForestClassifier(n_jobs=-1, random_state=42),
    RidgeClassifier(random_state=42),
    KNeighborsClassifier(n_neighbors=10, n_jobs=-1),
    LinearSVC(dual='auto'),
    MultinomialNB(),
]
In [32]:
def fit_model(user_model):
    model = Pipeline([
        ('tfidf', TfidfVectorizer(tokenizer=lambda x: x, preprocessor=lambda x: x, token_pattern=None)),
        ('model', user_model),
    ]).fit(X_train, y_train)

    y_pred = model.predict(X_val)
    score = accuracy_score(y_pred, y_val)
    return model, score

def cv_model(user_model):
    model = Pipeline([
        ('tfidf', TfidfVectorizer(tokenizer=lambda x: x, preprocessor=lambda x: x, token_pattern=None)),
        ('model', user_model),
    ]).fit(X_train, y_train)

    kfold = StratifiedKFold(n_splits=5, random_state=42, shuffle=True)
    score = cross_val_score(model, X_train, y_train, cv=kfold, scoring='accuracy').mean()

    return model, score

Модели из списка ниже подбирались руками по двум критериям: точность предсказаний и скорость обучения. Также для сравнения были включены несколько стандартных решений для классификации.

3.1 LogisticRegression¶

In [33]:
%%time
model, score = cv_model(models[0])
print(f'{model[1].__class__.__name__}: {score:.4f}')
LogisticRegression: 0.8395
CPU times: user 1.62 s, sys: 246 ms, total: 1.86 s
Wall time: 27.4 s

3.2 ComplementNB¶

In [34]:
%%time
model, score = cv_model(models[1])
print(f'{model[1].__class__.__name__}: {score:.4f}')
ComplementNB: 0.8572
CPU times: user 1.58 s, sys: 57.3 ms, total: 1.64 s
Wall time: 1.67 s

3.3 SGDClassifier¶

In [35]:
%%time
model, score = cv_model(models[2])
print(f'{model[1].__class__.__name__}: {score:.4f}')
SGDClassifier: 0.8612
CPU times: user 2.68 s, sys: 45.5 ms, total: 2.72 s
Wall time: 1.83 s

3.4 RandomForestClassifier¶

In [36]:
%%time
model, score = cv_model(models[3])
print(f'{model[1].__class__.__name__}: {score:.4f}')
RandomForestClassifier: 0.7937
CPU times: user 2min 50s, sys: 914 ms, total: 2min 51s
Wall time: 26.3 s

3.5 RidgeClassifier¶

In [37]:
%%time
model, score = cv_model(models[4])
print(f'{model[1].__class__.__name__}: {score:.4f}')
RidgeClassifier: 0.8632
CPU times: user 9.29 s, sys: 70.8 ms, total: 9.36 s
Wall time: 4.39 s

3.6 KNeighborsClassifier¶

In [38]:
%%time
model, score = cv_model(models[5])
print(f'{model[1].__class__.__name__}: {score:.4f}')
KNeighborsClassifier: 0.8001
CPU times: user 11.9 s, sys: 2.32 s, total: 14.2 s
Wall time: 8.04 s

3.7 LinearSVC¶

In [39]:
%%time
model, score = cv_model(models[6])
print(f'{model[1].__class__.__name__}: {score:.4f}')
LinearSVC: 0.8590
CPU times: user 3.15 s, sys: 29.9 ms, total: 3.18 s
Wall time: 3.2 s

3.8 MultinomialNB¶

In [40]:
%%time
model, score = cv_model(models[7])
print(f'{model[1].__class__.__name__}: {score:.4f}')
MultinomialNB: 0.8265
CPU times: user 1.66 s, sys: 23.9 ms, total: 1.69 s
Wall time: 1.72 s

3.9 Обучение Всех Моделей¶

In [41]:
print(' ' + '_'*49)
all_models_scores = []
for model in models:
    model_name = model.__class__.__name__
    print(f'| Модель {model_name:<40} |')
    
    start_time = time.time()
    model, score = cv_model(model)
    end_time = time.time()
    learning_time = end_time - start_time


    print(f'|   {score=:.4f} {learning_time=:<18.4f} |', end=f"\n {'_'*49}\n")
    all_models_scores.append({'name': model_name, 'score': score, 'time': learning_time})
 _________________________________________________
| Модель LogisticRegression                       |
|   score=0.8395 learning_time=28.9804            |
 _________________________________________________
| Модель ComplementNB                             |
|   score=0.8572 learning_time=1.6207             |
 _________________________________________________
| Модель SGDClassifier                            |
|   score=0.8612 learning_time=1.7788             |
 _________________________________________________
| Модель RandomForestClassifier                   |
|   score=0.7937 learning_time=25.7424            |
 _________________________________________________
| Модель RidgeClassifier                          |
|   score=0.8632 learning_time=4.3032             |
 _________________________________________________
| Модель KNeighborsClassifier                     |
|   score=0.8001 learning_time=7.9358             |
 _________________________________________________
| Модель LinearSVC                                |
|   score=0.8590 learning_time=3.1977             |
 _________________________________________________
| Модель MultinomialNB                            |
|   score=0.8265 learning_time=1.5666             |
 _________________________________________________
In [42]:
df_models = pd.DataFrame(all_models_scores)
df_models = df_models.sort_values('score', ascending=False).reset_index(drop=True)
df_models
Out[42]:
name score time
0 RidgeClassifier 0.863213 4.303212
1 SGDClassifier 0.861210 1.778780
2 LinearSVC 0.858973 3.197689
3 ComplementNB 0.857203 1.620651
4 LogisticRegression 0.839499 28.980408
5 MultinomialNB 0.826500 1.566625
6 KNeighborsClassifier 0.800084 7.935763
7 RandomForestClassifier 0.793747 25.742447

Посмотрим на лучшую модель, это метрика для оценки моделей, предложенная создателями датасета

In [43]:
best_model, score = cv_model(models[4])
y_pred = best_model.predict(X_val)
best_model[1].__class__.__name__
Out[43]:
'RidgeClassifier'
In [44]:
print(classification_report(y_val, y_pred))
              precision    recall  f1-score   support

           0       0.89      0.89      0.89       518
           1       0.89      0.87      0.88       603
           2       0.93      0.88      0.90       490
           3       0.91      0.97      0.94       505
           4       0.82      0.81      0.81       543
           5       0.72      0.80      0.76       552
           6       0.84      0.85      0.84       557
           7       0.91      0.87      0.89       580
           8       0.81      0.80      0.81       577
           9       0.90      0.92      0.91       544
          10       0.97      0.95      0.96       586
          11       0.92      0.86      0.89       554
          12       0.87      0.89      0.88       546

    accuracy                           0.87      7155
   macro avg       0.88      0.87      0.87      7155
weighted avg       0.87      0.87      0.87      7155

Модель ошибается примерно одинаково на каждом классе

In [45]:
cm = confusion_matrix(y_val, y_pred)
fig = px.imshow(cm, x=le.classes_, y=le.classes_, text_auto=True, width=750, height=750)
fig.update_layout(title=f'Confusion Matrix Heatmap – {best_model[1].__class__.__name__}', xaxis_title='Предсказание', yaxis_title='Правда', title_x=0.5)
fig.show()
No description has been provided for this image
In [46]:
%%time
visualizer = LearningCurve(best_model, scoring='accuracy').fit(X, y).show()
No description has been provided for this image
CPU times: user 40.4 s, sys: 211 ms, total: 40.6 s
Wall time: 20.6 s

Но тут возникает проблема – переобучение

3.10 Результаты и выводы¶

In [47]:
df_models
Out[47]:
name score time
0 RidgeClassifier 0.863213 4.303212
1 SGDClassifier 0.861210 1.778780
2 LinearSVC 0.858973 3.197689
3 ComplementNB 0.857203 1.620651
4 LogisticRegression 0.839499 28.980408
5 MultinomialNB 0.826500 1.566625
6 KNeighborsClassifier 0.800084 7.935763
7 RandomForestClassifier 0.793747 25.742447

Судя по датафрейму, были получены хорошие результаты для классификации по 13 классам. К слову, некоторые стандартные модели отсутствуют в списке по причине очень долгого обучения и низких метрик, к примеру: MLPClassifier, GradientBoostingClassifier, SVC и т.п.


Глава 4. Улучшение и усовершенствование моделей

4.1 Выбор нескольких наиболее перспективных моделей на основе полученных результатов¶

Исходя из проведённых тестов, можно выделить несколько моделей, которые очнь хорошо себя показали

Ими являются:

  • RidgeClassifier
  • SGDClassifier
  • LinearSVC

Так как все модели обучаются довольно быстро, следующим шагом можно найти оптимальные гиперпараметры для каждой.

4.2 RidgeClassifier¶

Для начала оценим стандартную модель

In [48]:
model, score = fit_model(RidgeClassifier())
print(f'{model[1].__class__.__name__}: {score:.4f}')
RidgeClassifier: 0.8732

Теперь создадим функцию для поиска оптимальных гиперпараметров

In [49]:
def objective(trial):
    params = {
        'alpha': trial.suggest_float('alpha', 1e-2, 8, log=True),
        'tol': trial.suggest_float('tol', 1e-5, 1e-1, log=True),
        'solver': trial.suggest_categorical('solver', ['auto', 'cholesky', 'lsqr', 'sparse_cg', 'sag', 'saga']),
        'max_iter': 5000,
    }

    if params['solver'] in ['svd', 'saga', 'cholesky']:
        params['fit_intercept'] = False

    model = Pipeline([
        ('tfidf', TfidfVectorizer(tokenizer=lambda x: x, preprocessor=lambda x: x, token_pattern=None)),
        ('model', RidgeClassifier(**params)),
    ])

    kfold = StratifiedKFold(n_splits=5, random_state=42, shuffle=True)
    score = cross_val_score(model, X_train, y_train, cv=kfold, scoring='accuracy').mean()
    
    return score
In [50]:
study = optuna.create_study(direction='maximize')
# study.optimize(objective, n_trials=100)
[I 2024-05-06 12:27:45,354] A new study created in memory with name: no-name-b7cae6f6-509a-4987-ac55-8bfbfd3af286
In [51]:
# study.best_value, study.best_params

Лучшая модель выглядит так:

(0.8640980711789646,
 {'alpha': 1.1234072317796635,
  'tol': 0.043383626047401036,
  'solver': 'sparse_cg'})
In [52]:
best_params = {'alpha': 1.1234072317796635, 'tol': 0.043383626047401036, 'solver': 'sparse_cg'}
model, score_2 = fit_model(RidgeClassifier(**best_params))
print(f'{model[1].__class__.__name__}: {score_2:.4f}\nOld: {score:.4f}')
RidgeClassifier: 0.8718
Old: 0.8732

4.3 SGDClassifier¶

In [53]:
model, score = fit_model(SGDClassifier())
print(f'{model[1].__class__.__name__}: {score:.4f}')
SGDClassifier: 0.8671
In [54]:
def objective(trial):
    params = {
        'loss': trial.suggest_categorical('loss', ['log_loss', 'huber', 'modified_huber', 'epsilon_insensitive', 'hinge', 'perceptron']),
        'alpha': trial.suggest_float('alpha', 1e-6, 8, log=True),
        'penalty': trial.suggest_categorical('penalty', ['elasticnet', 'l1', 'l2']),
        'l1_ratio': trial.suggest_float('l1_ratio', 0, 1),
        'max_iter': 10000
    }

    model = Pipeline([
        ('tfidf', TfidfVectorizer(tokenizer=lambda x: x, preprocessor=lambda x: x, token_pattern=None)),
        ('model', SGDClassifier(**params)),
    ])

    kfold = StratifiedKFold(n_splits=5, random_state=42, shuffle=True)
    score = cross_val_score(model, X_train, y_train, cv=kfold, scoring='accuracy').mean()
    
    return score
In [55]:
study = optuna.create_study(direction='maximize')
# study.optimize(objective, n_trials=100)
[I 2024-05-06 12:27:46,649] A new study created in memory with name: no-name-1d00f4ac-459c-4916-a0c2-e48dbe190145
In [56]:
# study.best_value, study.best_params

Лучшая модель выглядит так:

(0.8624209114775152,
 {'loss': 'hinge',
  'alpha': 7.966067426682004e-05,
  'penalty': 'l2',
  'l1_ratio': 0.9717104158510521})
In [57]:
best_params = {'loss': 'hinge', 'alpha': 7.966067426682004e-05, 'penalty': 'l2', 'l1_ratio': 0.9717104158510521}
model, score_2 = fit_model(SGDClassifier(**best_params))
print(f'{model[1].__class__.__name__}: {score_2:.4f}\nOld: {score:.4f}')
SGDClassifier: 0.8671
Old: 0.8671

4.4 LinearSVC¶

In [58]:
model, score = fit_model(LinearSVC(max_iter=10000, dual='auto'))
print(f'{model[1].__class__.__name__}: {score:.4f}')
LinearSVC: 0.8678
In [59]:
def objective(trial):
    params = {
        'C': trial.suggest_float('C', 1e-5, 8, log=True),
        'tol': trial.suggest_float('tol', 1e-5, 1e-1, log=True),
        'dual': trial.suggest_categorical('dual', [True, False]),
        'max_iter': 20000,
    }
    
    model = Pipeline([
        ('tfidf', TfidfVectorizer(tokenizer=lambda x: x, preprocessor=lambda x: x, token_pattern=None)),
        ('model', LinearSVC(**params)),
    ])

    kfold = StratifiedKFold(n_splits=5, random_state=42, shuffle=True)
    score = cross_val_score(model, X_train, y_train, cv=kfold, scoring='accuracy').mean()
    
    return score
In [60]:
study = optuna.create_study(direction='maximize')
# study.optimize(objective, n_trials=100)
[I 2024-05-06 12:27:47,983] A new study created in memory with name: no-name-b04b7520-5a4d-46fb-a4ee-f148895f5487
In [61]:
# study.best_value, study.best_params

Лучшая модель выглядит так:

(0.8620946906568248,
 {'C': 0.278178921051732,
 'tol': 0.004207430596057012,
 'dual': True})
In [62]:
best_params = {'C': 0.278178921051732, 'tol': 0.004207430596057012, 'dual': True}
model, score_2 = fit_model(LinearSVC(**best_params))
print(f'{model[1].__class__.__name__}: {score_2:.4f}\nOld: {score:.4f}')
LinearSVC: 0.8688
Old: 0.8678

4.5 Результаты и выводы¶

Как видно из результатов поиск параметров почти не увеличил точность лучших моделей.

Возможно стоит рассмотреть другие подходы с применением нейронных сетей и различных способов векторизации текста.


Глава 5. Модели huggingface.co

Ещё одним подходом к классификации текстов является дообучение нейросетевых моделей. Так как язык датасета русский, нужно использовать модели с поддержкой русского языка.

В данной задаче лучшую результативность показали две модели:

  • ruBert-large
  • ruRoberta-large

Именно они и будут рассмотрены.

5.1 Подготовка к дообучению¶

Укажем девайс на котором будем производить все операции, в данном случае это GPU (cuda)

In [59]:
device = torch.device('cuda' if torch.cuda.is_available() else 'cpu')
device
Out[59]:
device(type='cuda')
In [60]:
train_data.head(3)
Out[60]:
oid category text category_le length text_tokenized text_tokenized_joined
0 365271984 winter_sport Волшебные фото Виктория Поплавская ЕвгенияМедв... 12 65 [волшебный, фото, виктория, поплавский, евгени... волшебный фото виктория поплавский евгениямедв...
1 503385563 extreme Возвращение в подземелье Треша 33 Эйфория тупо... 5 246 [возвращение, подземелье, треш, эйфория, тупос... возвращение подземелье треш эйфория тупость жа...
2 146016084 football Лучшие чешские вратари – Доминик Доминатор Гаш... 6 704 [хороший, чешский, вратарь, доминик, доминатор... хороший чешский вратарь доминик доминатор гаше...

Разделим датасет на 3 выборки в соотношении 80/10/10% и преобразуем их в объекты класса Dataset

In [61]:
X, y = train_data['text'].tolist(), train_data['category_le'].tolist()
X_train, X_test_val, y_train, y_test_val = train_test_split(X, y, test_size=0.2, random_state=42, stratify=y)
X_test, X_val, y_test, y_val = train_test_split(X_test_val, y_test_val, test_size=0.5, random_state=42, stratify=y_test_val)
del X_test_val, y_test_val  # удалим эти переменные, чтобы их после случайно не использовать

data_dict_train = {'text': X_train, 'label': y_train}
data_dict_val = {'text': X_val, 'label': y_val}
data_dict_test = {'text': X_test, 'label': y_test}

dataset_train = Dataset.from_dict(data_dict_train)
dataset_val = Dataset.from_dict(data_dict_val)
dataset_test = Dataset.from_dict(data_dict_test)
In [62]:
dataset_train
Out[62]:
Dataset({
    features: ['text', 'label'],
    num_rows: 28619
})

В предыдущей части работы проводилась предобработка датасета, были удалены стоп слова, ненужные числа, была использована лемматизация, но в данном случае всё это не требуется, так как токенизатор модели выполняет эту работу

In [63]:
data = dataset_train[:5]
for text, label in zip(data['text'], data['label']):
    le_label = le.inverse_transform([label])[0]
    print(f'{le_label:^14} | {text}')
  volleyball   | Мэтью Андерсон возвращается в Казань 33 Но в составе другой команды. ‼Не пропустите сегодняшний матч. который обещает быть максимально жарким 33 Зенит Казань Казань Зенит Санкт Петербург 19 30 29. 10. 2022 Прямая трансляция
    hockey     | Трактор Ltokenoid 33 I vs Авангард ️ ⠀ Гостем онлайн эфира станет экс нападающий Трактора и Авангарда Николай Лемтюгов. Михаил Зислис и Елизавета Смолина обсудят с хоккейным экспертом Денисом Мошаровым прошедший матч команды и поделятся впечатлениями об игре сегодняшнего соперника. ⠀ В конкурсе прогнозов вас ждут мнения журналистов изданий Спорт Экспресс и tokentokenoid Михаила Скрыля и Дарьи Тубольцевой. ⠀ Прямая трансляция студии в официальной группе клуба ВКонтакте и на Yotokenoid канале Трактор SHOW начнется за 35 минут перед началом матча и продолжится в каждом из перерывов. хктрактор толькочернобелый
    esport     | Три матча и три путевки на IEM Rtokenoid Major пятый и финальный раунд Etokenoid RMR A стартует совсем скоро 33 Страница турнира tokentokenoid IEM
   extreme     | Друзья кидайте в предложку свои фото и видео с тренировок и соревнований. Посмотрим кто на что горазд в нашей дружине Приветствую друзья 33 Значительно улучшил свой результат в отжиманиях на брусьях с дополнительным весом 32 кг на разы 33 Новому личному рекорду быть
 winter_sport  | ОМОН и Новый год К обеспечению безопасности гостей горнолыжного курорта Шерегеш в период новогодних праздников привлекли ОМОН. Для охраны общественного порядка и обеспечения безопасности гостей горнолыжного курорта задействуют силы и средства ОМОН. В новогоднюю ночь в регионе к охране общественного порядка были привлечены более 400 бойцов Росгвардии. Среди них подразделения ОМОН вневедомственной охраны и лицензионно разрешительной работы. Кроме Росгвардии правоохранительным органам оказали содействие работники частных охранных организаций а также представители добровольных народных дружин.

Также стоит подготовить метрику для оценки наших моделей, в данном случае это accuracy.

In [64]:
metric = evaluate.load('accuracy')
In [65]:
def compute_metrics(eval_pred):
    logits, labels = eval_pred
    predictions = np.argmax(logits, axis=-1)
    return metric.compute(predictions=predictions, references=labels)

5.2 ruBert-large¶

Для начала тоекнизируем наш датасет

In [66]:
tokenizer = AutoTokenizer.from_pretrained('ai-forever/ruBert-large')

def tokenize_function(examples):
    return tokenizer(examples['text'], padding='max_length', truncation=True, max_length=512)

dataset_train = dataset_train.map(tokenize_function, batched=True)
dataset_val = dataset_val.map(tokenize_function, batched=True)
dataset_test = dataset_test.map(tokenize_function, batched=True)
Map: 100%|██████████| 28619/28619 [00:07<00:00, 3719.53 examples/s]
Map: 100%|██████████| 3578/3578 [00:00<00:00, 3945.21 examples/s]
Map: 100%|██████████| 3577/3577 [00:00<00:00, 3913.20 examples/s]

После токенизации помимо столбцов 'text' и 'label' появились еще 'input_ids', 'token_type_ids', 'attention_mask'.

In [67]:
dataset_train
Out[67]:
Dataset({
    features: ['text', 'label', 'input_ids', 'token_type_ids', 'attention_mask'],
    num_rows: 28619
})

Датасет уже предобработан, осталось только загрузить предобученную модель.

Но перед дообучением нужно указать что:

  • num_labels=13 - Количество классов нашей модели изменилось
  • ignore_mismatched_sizes=True - Отключаем исключение при несовпадении некоторых весов
In [68]:
model = AutoModelForSequenceClassification.from_pretrained(
    'ai-forever/ruBert-large', num_labels=13
).to(device)
Some weights of BertForSequenceClassification were not initialized from the model checkpoint at ai-forever/ruBert-large and are newly initialized: ['classifier.bias', 'classifier.weight']
You should probably TRAIN this model on a down-stream task to be able to use it for predictions and inference.

Зададим аргументы для дообучения модели при помощи объекта класса TrainingArguments

In [69]:
training_args = TrainingArguments(
    num_train_epochs=4,
    warmup_steps=500,
    output_dir='hf_models',
    evaluation_strategy='steps',
    eval_steps=250,
    save_strategy='steps',
    save_steps=250,
    per_device_train_batch_size=8,
    per_device_eval_batch_size=8,
)

Создадим объект класса Trainer указав нашу модель, аргументы, датасеты и метрику

In [70]:
trainer = Trainer(
    model=model,
    args=training_args,
    train_dataset=dataset_train,
    eval_dataset=dataset_val,
    compute_metrics=compute_metrics,
)
c:\Основная папка\Рабочий стол\ml_coursework_2nd_year\.venv\Lib\site-packages\accelerate\accelerator.py:436: FutureWarning:

Passing the following arguments to `Accelerator` is deprecated and will be removed in version 1.0 of Accelerate: dict_keys(['dispatch_batches', 'split_batches', 'even_batches', 'use_seedable_sampler']). Please pass an `accelerate.DataLoaderConfiguration` instead: 
dataloader_config = DataLoaderConfiguration(dispatch_batches=None, split_batches=False, even_batches=True, use_seedable_sampler=True)

А теперь запустим процесс дообучения

In [71]:
# trainer.train()

Лучшая модель выглядит так:

{
    "epoch": 3.98,
    "eval_accuracy": 0.8806595863610955,
    "eval_loss": 0.7195928692817688,
    "eval_runtime": 240.8435,
    "eval_samples_per_second": 14.856,
    "eval_steps_per_second": 1.86,
    "step": 14250
}

Теперь загрузим чекпоинт лучшей модели, чтобы оценить качество предсказаний

In [72]:
model = AutoModelForSequenceClassification.from_pretrained(
    'hf_models/ruBert-large-checkpoint-14250',
).to(device)
In [73]:
trainer = Trainer(
    model=model,
    args=training_args,
    train_dataset=dataset_train,
    eval_dataset=dataset_val,
    compute_metrics=compute_metrics,
)

Оценим модель на всех 3-х выборках

  1. Тренировочная:
In [74]:
ix = np.random.choice(range(len(dataset_train)), size=5000, replace=False)
trainer.evaluate(dataset_train.select(ix))
100%|██████████| 625/625 [05:51<00:00,  1.78it/s]
Out[74]:
{'eval_loss': 0.04601737856864929,
 'eval_accuracy': 0.9912,
 'eval_runtime': 352.1637,
 'eval_samples_per_second': 14.198,
 'eval_steps_per_second': 1.775}
  1. Валидационная:
In [75]:
trainer.evaluate(dataset_val)
100%|██████████| 448/448 [04:11<00:00,  1.78it/s]
Out[75]:
{'eval_loss': 0.7195214033126831,
 'eval_accuracy': 0.8806595863610955,
 'eval_runtime': 252.0094,
 'eval_samples_per_second': 14.198,
 'eval_steps_per_second': 1.778}
  1. Тестовая:
In [76]:
trainer.evaluate(dataset_test)
100%|██████████| 448/448 [04:12<00:00,  1.77it/s]
Out[76]:
{'eval_loss': 0.7208680510520935,
 'eval_accuracy': 0.8803466592116299,
 'eval_runtime': 253.3381,
 'eval_samples_per_second': 14.119,
 'eval_steps_per_second': 1.768}

Accuracy score на валидационной и тестовой выборках у данной модели немного выше (+0.7%) чем применение TF-IDF и моделей машинного обучения из библиотеки sklearn, но затраты времени и ресурсов на тренировку и предсказание значительно увеличилось.

Предскажем ещё раз тестовую выборку и выведем отчёт о классификации

In [77]:
predictions_output = trainer.predict(dataset_test)
logits = predictions_output.predictions
predicted_labels = np.argmax(logits, axis=-1)
100%|██████████| 448/448 [04:13<00:00,  1.77it/s]
In [78]:
print(classification_report(
    le.inverse_transform(dataset_test['label']),
    le.inverse_transform(predicted_labels),
))
              precision    recall  f1-score   support

   athletics       0.91      0.90      0.90       259
   autosport       0.88      0.88      0.88       301
  basketball       0.88      0.84      0.86       245
  boardgames       0.96      0.96      0.96       253
      esport       0.85      0.84      0.85       272
     extreme       0.77      0.82      0.79       276
    football       0.83      0.84      0.84       278
      hockey       0.86      0.89      0.88       289
martial_arts       0.84      0.84      0.84       288
   motosport       0.96      0.94      0.95       272
      tennis       0.95      0.94      0.95       293
  volleyball       0.87      0.86      0.86       278
winter_sport       0.89      0.90      0.90       273

    accuracy                           0.88      3577
   macro avg       0.88      0.88      0.88      3577
weighted avg       0.88      0.88      0.88      3577

Модель предсказывает все классы с примерно одинаковой точностью, что является хорошим показателем

5.3 ruRoberta-large¶

Совершим те же шаги, что и для предыдущей модели

In [79]:
tokenizer = AutoTokenizer.from_pretrained('ai-forever/ruRoberta-large')

def tokenize_function(examples):
    return tokenizer(examples['text'], padding='max_length', truncation=True, max_length=512)

dataset_train = dataset_train.map(tokenize_function, batched=True)
dataset_val = dataset_val.map(tokenize_function, batched=True)
dataset_test = dataset_test.map(tokenize_function, batched=True)
Map: 100%|██████████| 28619/28619 [00:05<00:00, 5048.98 examples/s]
Map: 100%|██████████| 3578/3578 [00:00<00:00, 5364.25 examples/s]
Map: 100%|██████████| 3577/3577 [00:00<00:00, 5268.05 examples/s]
In [80]:
dataset_train
Out[80]:
Dataset({
    features: ['text', 'label', 'input_ids', 'token_type_ids', 'attention_mask'],
    num_rows: 28619
})
In [81]:
model = AutoModelForSequenceClassification.from_pretrained(
    'ai-forever/ruRoberta-large', num_labels=13, ignore_mismatched_sizes=True,
).to(device)
Some weights of RobertaForSequenceClassification were not initialized from the model checkpoint at ai-forever/ruRoberta-large and are newly initialized: ['classifier.dense.bias', 'classifier.dense.weight', 'classifier.out_proj.bias', 'classifier.out_proj.weight']
You should probably TRAIN this model on a down-stream task to be able to use it for predictions and inference.

Аргументы тренировки подбираются для каждой модели отдельно

In [82]:
training_args = TrainingArguments(
    num_train_epochs=10,
    warmup_steps=500,
    output_dir='hf_models',
    evaluation_strategy='steps',
    eval_steps=100,
    save_strategy='steps',
    save_steps=100,
    per_device_train_batch_size=8,
    per_device_eval_batch_size=8,
    gradient_accumulation_steps=8,
    fp16=True,
)
In [83]:
trainer = Trainer(
    model=model,
    args=training_args,
    train_dataset=dataset_train,
    eval_dataset=dataset_val,
    compute_metrics=compute_metrics,
)
c:\Основная папка\Рабочий стол\ml_coursework_2nd_year\.venv\Lib\site-packages\accelerate\accelerator.py:436: FutureWarning:

Passing the following arguments to `Accelerator` is deprecated and will be removed in version 1.0 of Accelerate: dict_keys(['dispatch_batches', 'split_batches', 'even_batches', 'use_seedable_sampler']). Please pass an `accelerate.DataLoaderConfiguration` instead: 
dataloader_config = DataLoaderConfiguration(dispatch_batches=None, split_batches=False, even_batches=True, use_seedable_sampler=True)

In [84]:
# trainer.train()

Лучшая модель выглядит так:

{
    "epoch": 9.17,
    "eval_accuracy": 0.8918390162101733,
    "eval_loss": 0.6570751070976257,
    "eval_runtime": 227.775,
    "eval_samples_per_second": 15.708,
    "eval_steps_per_second": 1.967,
    "step": 4100
}

Также загрузим лучшую модель с чекпоинта

In [85]:
model = AutoModelForSequenceClassification.from_pretrained(
    'hf_models/ruRoberta-large-checkpoint-4100',
).to(device)
In [86]:
trainer = Trainer(
    model=model,
    args=training_args,
    train_dataset=dataset_train,
    eval_dataset=dataset_val,
    compute_metrics=compute_metrics,
)

И оценим модель на всех 3-х выборках

  1. Тренировочная:
In [87]:
ix = np.random.choice(range(len(dataset_train)), size=5000, replace=False)
trainer.evaluate(dataset_train.select(ix))
100%|██████████| 625/625 [05:21<00:00,  1.94it/s]
Out[87]:
{'eval_loss': 0.004432776477187872,
 'eval_accuracy': 0.9982,
 'eval_runtime': 322.1358,
 'eval_samples_per_second': 15.521,
 'eval_steps_per_second': 1.94}
  1. Валидационная:
In [88]:
trainer.evaluate(dataset_val)
100%|██████████| 448/448 [03:47<00:00,  1.97it/s]
Out[88]:
{'eval_loss': 0.6570751070976257,
 'eval_accuracy': 0.8918390162101733,
 'eval_runtime': 227.8208,
 'eval_samples_per_second': 15.705,
 'eval_steps_per_second': 1.966}
  1. Тестовая:
In [89]:
trainer.evaluate(dataset_test)
100%|██████████| 448/448 [03:48<00:00,  1.96it/s]
Out[89]:
{'eval_loss': 0.6594090461730957,
 'eval_accuracy': 0.8951635448700028,
 'eval_runtime': 229.1215,
 'eval_samples_per_second': 15.612,
 'eval_steps_per_second': 1.955}

По сравнению с моделями sklearn точность предсказаний выросла на +2.2%, а это уже более весомый результат

Предскажем тестовую выборку и выведем отчёт о классификации

In [90]:
predictions_output = trainer.predict(dataset_test)
logits = predictions_output.predictions
predicted_labels = np.argmax(logits, axis=-1)
100%|██████████| 448/448 [03:50<00:00,  1.94it/s]
In [91]:
print(classification_report(
    le.inverse_transform(dataset_test['label']),
    le.inverse_transform(predicted_labels),
))
              precision    recall  f1-score   support

   athletics       0.93      0.92      0.92       259
   autosport       0.92      0.90      0.91       301
  basketball       0.87      0.87      0.87       245
  boardgames       0.97      0.95      0.96       253
      esport       0.86      0.88      0.87       272
     extreme       0.84      0.84      0.84       276
    football       0.83      0.85      0.84       278
      hockey       0.85      0.88      0.87       289
martial_arts       0.87      0.86      0.86       288
   motosport       0.95      0.95      0.95       272
      tennis       0.95      0.96      0.95       293
  volleyball       0.89      0.88      0.89       278
winter_sport       0.92      0.89      0.91       273

    accuracy                           0.90      3577
   macro avg       0.90      0.90      0.90      3577
weighted avg       0.90      0.90      0.90      3577

Ситуация по предсказанию всех классов аналогичная, только метрики чуть выше


Глава 6. Финальные результаты

По итогам можно составить такую таблицу

Модель Точность
ruRoberta-large 0.8952
ruBert-large 0.8801

Разница между лучшими моделями sklearn и transformers составляет целых 2.2%, что может оказать большое влияение на выбор модели.


Заключение

В ходе работы получилось построить модели для классификации текстов используя различные подходы.

 

Модели классификации sklearn показали довольно высокие результаты в случае использования TF-IDF с дополнительной предобработкой данных.

Лучшими из них оказались:

Модель Точность
RidgeClassifier 0.8732
LinearSVC 0.8678
SGDClassifier 0.8660

 

Также были применены нейросетевые модели классификации текстов такие как:

Модель Точность
ruRoberta-large 0.8952
ruBert-large 0.8801

 

Любой из этих подходов гарантирует довольно высокую точность классификации текстов по 13 категориям. Модели sklearn показали высокое быстродействие, при обучении и предсказании используется только центральный процессор. Модели transformers используют центральный процессор в связке с видеокартой, они значительно тяжелее чем модели sklearn, но дают более точные результаты.


Список использованных источников

NLP (Natural Language Processing, обработка естественного языка)

Краткий обзор техник векторизации в NLP

Репозиторий в поддержку курса "Машинное обучение", читаемого в Финансовом университете

Инструменты для решения NER-задач для русского языка

Проект Natasha. Набор качественных открытых инструментов для обработки естественного русского языка (NLP)

Natural Language Processing With Python's NLTK Package

Библиотека Optuna в Python для оптимизации гиперпараметров

Шпаргалка по визуализации данных в Python с помощью Plotly

Руформеры (Список популярных открытых базовых моделей на основе трансформеров)

Общие задачи NLP

Дообучение модели